﻿using Bettzueche.Dampfross;
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Bettzueche.DampfrossContinuum
{
    /// <summary>
    /// Lernagent für DampfrossKontinuum
    /// </summary>
    public class AgentC
    {
        KarteC _karte;
        EpsilonGreedy _policy;

        /// <summary>
        /// Sets the umwelt. Sollte nur zu Beginn einer Lernepisode und nicht währenddessen gesetzt werden.
        /// </summary>
        /// <value>
        /// The umwelt. Sollte nicht <c>null</c> sein.
        /// </value>
        public KarteC Umwelt {
            internal get { return _karte; }
            set {
                _karte = value;
            }
        }

        /// <summary>
        /// Gets or sets the Q function.
        /// </summary>
        /// <value>
        /// The Q function.
        /// </value>
        public Bettzueche.RLLibrary.Functions.IStateValueFunction<UmgebungC, Aktion> QFunction {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Eligibility function.
        /// </summary>
        /// <value>
        /// The E function.
        /// </value>
        public Bettzueche.RLLibrary.Functions.IEligibilityFunction<Aktion, UmgebungC> EFunction {
            get;
            set;
        }

        /// <summary>
        /// Gets the total reward.
        /// </summary>
        /// <value>
        /// The total reward.
        /// </value>
        public double RewardTotal { get; private set; }

        /// <summary>
        /// Gets or sets die Lernrate α. Standard = 0.5.
        /// </summary>
        /// <value>
        /// The α von 0.0 bis 1.0.
        /// </value>
        public double Alpha { get; set; }

        /// <summary>
        /// Gets or sets Abschwächungsfaktor γ. Standard = 0.95.
        /// Setzt auch den Wert für EFunction.DiscountFactor!
        /// </summary>
        /// <value>
        /// The γ von 0.0 bis 1.0.
        /// </value>
        public double Gamma {
            get { return _gamma; }
            set { 
                _gamma = value;
                if (EFunction != null)
                    EFunction.DiscountFactor = _gamma;
            }
        }
        double _gamma;

        /// <summary>
        /// Gets or sets the Spurzerfallsfaktor λ. Standard = 0.9.
        /// Setzt auch den Wert für EFunction.TraceDecay!
        /// </summary>
        /// <value>
        /// The λ von 0.0 bis 1.0.
        /// </value>
        public double Lambda {
            get { return _lambda; }
            set {
                _lambda = value;
                if (EFunction != null)
                    EFunction.TraceDecay = _lambda;
            }
        }
        double _lambda;

        /// <summary>
        /// Gets or sets minimaler E-Wert, der nicht zu 0 interpretiert wird.
        /// Standard = 0.9^50 = ca. 0,5 %
        /// Setzt auch den Wert für EFunction.MinE!
        /// </summary>
        /// <value>
        /// The minimum berechtigung.
        /// </value>
        public double MinBerechtigung {
            get { return _minE; }
            set {
                _minE = value;
                if (EFunction != null)
                    EFunction.MinE = _minE;
            }
        }
        double _minE;

        /// <summary>
        /// Gets or sets Die Explorationsrate ε. Standard = 0.1.
        /// </summary>
        /// <value>
        /// The ε von 0.0 bis 1.0.
        /// </value>
        public double Epsilon {
            get { return _epsilon; }
            set {
                _epsilon = value;
                _policy = new EpsilonGreedy(_epsilon);
            }
        }
        double _epsilon;

        /// <summary>
        /// Initializes a new instance of the <see cref="AgentC"/> class,
        /// mit Standardwerten und -klassen für die Eigenschaftsfelder.
        /// </summary>
        /// <param name="karte">Referenz auf die "Umwelt.</param>
        /// <exception cref="System.ArgumentNullException">karte</exception>
        public AgentC(KarteC karte) {
            if (karte == null) {
                throw new ArgumentNullException();
            }
            _karte = karte;
            InitKnowledgeBase();
            Alpha = 0.5;
            Gamma = 0.95; // Seiteneffekt auf QEFunction
            Lambda = 0.9; // Seiteneffekt auf QEFunction
            MinBerechtigung = Math.Pow(Lambda, 50);
            Epsilon = 0.1;
        }

        /// <summary>
        /// Does the step.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <returns>next action recommended</returns>
        public Aktion DoStep(Aktion action) {
            UmgebungC state = Umwelt.AktuellerZustand; //Environment.State;
            UmgebungC nextState;
            double reward; // = Environment.DoAction(action, out nextState);
            Umwelt.MacheSpielzug(action.Typ, out reward, out nextState);
            RewardTotal += reward;

            Aktion nextAction = _policy.GetAction(QFunction.GetEstimates(nextState)/*GibQWerte(nextState)*/);
            double delta = reward + Gamma * QFunction[nextState, nextAction] - QFunction[state, action];
            EFunction.Update(action, state); // E = γ·λ·E; e_sx = 0; e_sa = 1;
            QFunction.Add(EFunction, Alpha * delta); // W = W + α·δ·E
            /*
            QFunction.Values = QFunction.Values + (LearnRate * delta) * EFunction.Eligibilities;
            Path.Add(new SATupel<S, A>(state, action)); // add to visited (s,a)'s
            foreach (var tupel in Path) {
                // do update
            }
            */
            return nextAction;
        }
        
        /// <summary>
        /// Gibt die mittlere quadratische Abweichung der Q-Werte zu den Test-Zuständen.
        /// <see cref="SetStates"/>() - Die betroffenen Zustand-IDs müssen gesetzt sein.
        /// <see cref="SetSollwerte"/>() - Die zugehörigen Sollwerte müssen gesetzt sein.
        /// </summary>
        /// <returns>Mitllere quadratische Abweichung aller (beteiligten) Q(s,a)-Werte</returns>
        internal double GetMSE() {
            double sqDiffs = 0.0;
            foreach (var state in _debugStates) {
                for (int a = 0; a < 6; a++) {
                    double ist = QFunction[state.Value, (Aktion)a];
                    double soll = -_debugSollwerte[state.Key][a];
                    double diff = ist - soll; // soll ist positiv, W aber negativ angegeben!
                    sqDiffs += diff * diff;
                }
            }
            sqDiffs = sqDiffs / (_debugStates.Count * 6);
            return sqDiffs;
        }



        // DebugPrint:
        //static readonly ConsoleColor badRed = ConsoleColor.Red;
        //static readonly ConsoleColor nearYellow = ConsoleColor.Yellow;
        //static readonly ConsoleColor goodGreen = ConsoleColor.Green;
        //static readonly ConsoleColor standardWhite = ConsoleColor.White;

        ////[Conditional("DEBUG")]
        //double DebugPrint(HexRichtungen lastAction) {
        //    HexKoordinate actualPosition = _karte._aktuellePosition;
        //    Console.CursorLeft = 0;
        //    Console.CursorTop = 0;
        //    Console.WriteLine("letzte Aktion: {0,2} / akt. Position: {1,-11}", lastAction, actualPosition);
        //    int stateCnt = 0;
        //    double sqDiffs = 0.0;
        //    foreach (int s in _debugStates) {
        //        Console.ForegroundColor = AgentC.standardWhite;
        //        Console.WriteLine("s#{0:N0}", s);
        //        Console.Write("\t");
        //        for (int a = 0; a < 6; a++) {
        //            WETupel we = _qeMatrix[a][s];
        //            Console.ForegroundColor = AgentC.standardWhite;
        //            Console.Write("{0}:(", (HexRichtungen)a);
        //            double soll = _debugSollwerte[stateCnt][a];
        //            double diff = Math.Abs(-soll - we.W); // soll ist positiv, W negativ angegeben!
        //            sqDiffs += (-soll - we.W) * (-soll - we.W);
        //            if (diff <= (0.01 * soll))
        //                Console.ForegroundColor = AgentC.standardWhite;
        //            else if (diff <= (0.1 * soll))
        //                Console.ForegroundColor = AgentC.goodGreen;
        //            else if (diff <= (0.2 * soll))
        //                Console.ForegroundColor = AgentC.nearYellow;
        //            else
        //                Console.ForegroundColor = AgentC.badRed;
        //            Console.Write("{0,7:N3}", we.W);
        //            Console.ForegroundColor = AgentC.standardWhite;
        //            Console.Write(" | {0:N3}) ", we.E);
        //            if (a == 2) {
        //                Console.WriteLine();
        //                Console.Write("\t");
        //            }
        //        }
        //        Console.WriteLine();
        //        stateCnt++;
        //    }
        //    sqDiffs /= stateCnt * 6;
        //    Console.WriteLine("MSE: {0:N3}", sqDiffs);
        //    //System.Threading.Thread.Sleep(300);
        //    //Console.Write("Press any key to continue... ");
        //    //Console.ReadKey(true);
        //    return sqDiffs;
        //}

        /// <summary>
        /// Nur zu Testzwecken / für die Experimente!<para>
        /// Setze die Zustände, die ausgewertet werden sollen. Normalerweise alle fIDs, die zur
        /// jeweiligen Umwelt gehören und in der Datei zIDshexMapNxM-nn.csv gespeichert sind.
        /// </para><para>
        /// Für diese zustände wird in <c>GetMSE</c> der mittlere Error ermittelt.
        /// </para>
        /// </summary>
        /// <param name="states">Die Testzustände. Key=UmgebungC.FeaturedID, Value=UmgebungC</param>
        [Conditional("DEBUG")]
        internal void SetStates(Dictionary<int,UmgebungC> states) {
            _debugStates = states;
        }
        Dictionary<int, UmgebungC> _debugStates;


        /// <summary>
        /// Nur zu Testzwecken / für die Experimente!<para>
        /// Setze die Sollwerte, die ausgewertet werden sollen. Normalerweise sortiert nach Zick-Zack-Reihendurchlauf
        /// durch die Karte und gespeichert in SollwerteNxM.csv.
        /// </para><para>
        /// Diese Sollwerte werden in <c>GetMSE</c> herangezogen.
        /// </para>
        /// </summary>
        /// <param name="sollwerte">Eine mit FeaturedId indizierte Liste der Q(s,a)-Sollwerte.
        ///                         Die Aktionen sollten die Reihenfolge wie im Enum HexRichtungen (von NO guzs)</param>
        [Conditional("DEBUG")]
        internal void SetSollwerte(Dictionary<int,double[]> sollwerte) {
            _debugSollwerte = sollwerte;
        }
        Dictionary<int, double[]> _debugSollwerte;

        /// <summary>
        /// Runs the episode.
        /// </summary>
        /// <param name="maxActionCount">The maximum action count.</param>
        /// <returns>Anzahl ausgeführter Schritte</returns>
        public int RunEpisode(int maxActionCount) {
            // alles Initialisiert?
            //int sIdx = GibZustandsIndex(_karte.AktuellerZustand);
            var state = _karte.AktuellerZustand;
            HexRichtungen action = _policy.GetAction(QFunction.GetEstimates(state)/*GibQWerte(sIdx)*/); // Q-Werte von Zustand #sIdx
            int stepCount = 0;
            while ((!_karte.IsFinal) && stepCount < maxActionCount) {
                action = this.DoStep(action);
                stepCount++;
            }
            return stepCount;
        }

        //public void RunDebugEpisode(int maxActionCount, out int stepCount, out double reward, out double mse) {
        //    stepCount = RunEpisode(maxActionCount);
        //    // ---------------------------------------------
        //    mse = DebugPrint(HexRichtungen.NO);
        //    // --------------------------------------------
        //    reward = RewardTotal;
        //}

        /// <summary>
        /// Agent (bei Episodenende) zurücksetzen.<para>
        /// RewardTotal = 0. Gelerntes bleibt erhalten! Properties auch.</para>
        /// </summary>
        public void Reset() {
            EFunction.Clear(); // Lohnberechtigung (eligibility) zurücksetzen
            RewardTotal = 0;
        }

        /// <summary>
        /// Amnesie! Setzt den Agent vollständig zurück und löscht dabei auch das Gelernte!
        /// Properties bleiben erhalten.
        /// </summary>
        public void Clear() {
            InitKnowledgeBase();
            RewardTotal = 0;
        }

        // SafeExperiance LoadExperiance
        ///// <summary>
        ///// Safes the experiance.
        ///// </summary>
        ///// <param name="filename">The filename.</param>
        ///// <exception cref="System.Runtime.Serialization.SerializationException">
        ///// An error has occurred during serialization, such as if an object in the graph
        /////    parameter is not marked as serializable.
        ///// </exception>
        ///// <exception cref="System.Security.SecurityException">
        ///// The caller does not have the required permission.
        ///// </exception>
        //public void SafeExperiance(string filename) {
        //    Stream stream = File.Open(filename, FileMode.Create);
        //    BinaryFormatter bFormatter = new BinaryFormatter();
        //    double[][] qWerte = new double[_qeMatrix[0].Length][];
        //    for (int i = 0; i < qWerte.Length; i++) {
        //        qWerte[i] = new double[_qeMatrix[1].Length];
        //    }
        //    for (int a = 0; a < _qeMatrix.GetLength(0); a++) {
        //        for (int s = 0; s < _qeMatrix.GetLength(1); s++) {
        //            qWerte[a][s] = _qeMatrix[a][s].W;
        //        }
        //    }
        //    try {
        //        bFormatter.Serialize(stream, qWerte);
        //    }
        //    catch (Exception) {
        //        throw;
        //    }
        //    finally {
        //        stream.Close();
        //    }
        //}

        // LoadExperiance
        ///// <summary>
        ///// Loads the experiance.
        ///// </summary>
        ///// <param name="filename">The filename.</param>
        ///// <exception cref="System.Runtime.Serialization.SerializationException">
        ///// The serializationStream supports seeking, but its length is 0
        ///// </exception>
        ///// <exception cref="System.Security.SecurityException">
        ///// The caller does not have the required permission.
        ///// </exception>
        //public void LoadExperiance(string filename) {
        //    double[][] qWerte;
        //    Stream stream = File.Open(filename, FileMode.Open);
        //    BinaryFormatter bFormatter = new BinaryFormatter();
        //    try {
        //        qWerte = (double[][])bFormatter.Deserialize(stream);
        //    }
        //    catch (Exception) {
        //        throw;
        //    }
        //    finally {
        //        stream.Close();
        //    }
        //    _qeMatrix = new WETupel[qWerte.GetLength(0)][];
        //    for (int i = 0; i < _qeMatrix.Length; i++) {
        //        _qeMatrix[i] = new WETupel[qWerte.GetLength(1)];
        //    }
        //    for (int a = 0; a < qWerte.GetLength(0); a++) {
        //        for (int s = 0; s < qWerte.GetLength(1); s++) {
        //            _qeMatrix[a][s] = new WETupel(qWerte[a][s], 0.0);
        //        }
        //    }
        //}
        

        /// <summary>
        /// Initialisiert die Q-Funktion (_qeMatrix) mit 0.
        /// </summary>
        private void InitKnowledgeBase() {
            var qeFunc = new QEFunktion();
            QFunction = qeFunc;
            EFunction = qeFunc;
            EFunction.DiscountFactor = this.Gamma;
            EFunction.TraceDecay = this.Lambda;
            EFunction.MinE = this.MinBerechtigung;
        }


    }
}
